import CanvasContext = WechatMiniprogram.CanvasContext
import Canvas = WechatMiniprogram.Canvas
import { Video } from '../Parser/video'
import { Sprite } from '../Parser/sprite'
import { BezierPath } from '../Parser/bezierPath'
import { EllipsePath } from '../Parser/ellipsePath'
import { RectPath } from '../Parser/rectPath'
import { IDynamicText, IPoint, ITransform } from '../Types/index'

const validMethods = 'MLHVCSQRZmlhvcsqrz'

export class Render {
  videoItem: Video
  ctx: CanvasContext
  canvas: Canvas
  globalTransform?: ITransform
  
  private dynamicImage: { [key: string]: any } = {}
  private dynamicText: { [key: string]: IDynamicText } = {}
  
  constructor(videoItem: Video, ctx: CanvasContext, canvas: Canvas) {
    this.videoItem = videoItem
    this.ctx = ctx
    this.canvas = canvas
  }
  
  // 清除画布
  clear() {
    const { ctx, canvas } = this
    ctx.clearRect(0.0, 0.0, canvas.width, canvas.height)
  }
  
  drawFrame(frame: number) {
    this.clear()
    
    const matteSprites: any = {}
    let isMatting = false
    const sprites = this.videoItem.sprites
    sprites.forEach((v: Sprite, i: number) => {
      if (sprites[0].imageKey?.indexOf('.matte') === -1) {
        this.drawSprite(v, frame)
        return
      }
      
      if (v.imageKey?.indexOf('.matte') !== -1) {
        matteSprites[v.imageKey!] = v
        return
      }
      
      const lastSprite = sprites[i - 1]
      
      if (isMatting && (!v.matteKey || v.matteKey.length === 0 || v.matteKey !== lastSprite.matteKey)) {
        isMatting = false
        
        const matteSprite = matteSprites[v.matteKey]
        this.ctx.globalCompositeOperation = 'destination-in'
        this.drawSprite(matteSprite, frame)
        this.ctx.globalCompositeOperation = 'source-over'
        this.ctx.restore()
      }
      
      if (v.matteKey !== null && (lastSprite.matteKey === null || lastSprite.matteKey.length === 0 || lastSprite.matteKey !== v.matteKey)) {
        isMatting = true
        this.ctx.save()
      }
      
      this.drawSprite(v, frame)
      
      if (isMatting && i === sprites.length - 1) {
        const matteSprite = matteSprites[v.matteKey]
        this.ctx.globalCompositeOperation = 'destination-in'
        this.drawSprite(matteSprite, frame)
        this.ctx.globalCompositeOperation = 'source-over'
        this.ctx.restore()
      }
    })
  }
  
  drawSprite(sprite: Sprite, frame: number) {
    const frameItem = sprite.frames[frame]
    if (frameItem.alpha < 0.05) return
    
    this.ctx.save()
    if (this.globalTransform) {
      const { a, b, c, d, tx, ty } = this.globalTransform
      this.ctx.transform(a, b, c, d, tx, ty)
    }
    this.ctx.globalAlpha = frameItem.alpha
    const { a, b, c, d, tx, ty } = frameItem.transform
    this.ctx.transform(a, b, c, d, tx, ty)
    
    const bitmapKey = sprite.imageKey?.replace('.matte', '')
    if (!bitmapKey) return
    const image = this.dynamicImage[bitmapKey] ?? this.videoItem.decodedImages[bitmapKey]
    
    if (frameItem.maskPath !== undefined && frameItem.maskPath !== null) {
      frameItem.maskPath._styles = undefined
      this.drawBezier(frameItem.maskPath)
      this.ctx.clip()
    }
    
    if (image) this.ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, frameItem.layout.width, frameItem.layout.height)
    
    if (frameItem?.shapes) {
      for (let i = 0; i < frameItem.shapes.length; i++) {
        const shape = frameItem.shapes[i]
        
        if (shape.type === 'shape' && shape.pathArgs && shape.pathArgs.d) {
          this.drawBezier(new BezierPath(shape.pathArgs.d, shape.transform, shape.styles))
        }
        
        if (shape.type === 'ellipse' && shape.pathArgs) {
          const x = parseFloat(shape.pathArgs.x) || 0.0
          const y = parseFloat(shape.pathArgs.y) || 0.0
          const radiusX = parseFloat(shape.pathArgs.radiusX) || 0.0
          const radiusY = parseFloat(shape.pathArgs.radiusY) || 0.0
          this.drawEllipse(new EllipsePath(x, y, radiusX, radiusY, shape.transform, shape.styles))
        }
        
        if (shape.type === 'rect' && shape.pathArgs) {
          const x = parseFloat(shape.pathArgs.x) || 0.0
          const y = parseFloat(shape.pathArgs.y) || 0.0
          const width = parseFloat(shape.pathArgs.width) || 0.0
          const height = parseFloat(shape.pathArgs.height) || 0.0
          const cornerRadius = parseFloat(shape.pathArgs.cornerRadius) || 0.0
          this.drawRect(new RectPath(x, y, width, height, cornerRadius, shape.transform, shape.style))
        }
      }
    }
    
    const dynamicText = this.dynamicText[bitmapKey]
    if (dynamicText !== undefined) {
      this.ctx.font = `${dynamicText.size}px ${dynamicText.family ?? 'Arial'}`
      const txtWidth = this.ctx.measureText(dynamicText.text).width
      this.ctx.fillStyle = dynamicText.color
      let offsetX = 0
      let offsetY = 0
      if (dynamicText.offset !== undefined && dynamicText.offset.x !== undefined && isNaN(dynamicText.offset.x)) offsetX = dynamicText.offset.x
      if (dynamicText.offset !== undefined && dynamicText.offset.y !== undefined && isNaN(dynamicText.offset.y)) offsetY = dynamicText.offset.y
      const { width, height } = frameItem.layout
      this.ctx.fillText(dynamicText.text, (width - txtWidth) / 2 + offsetX, height / 2 + offsetY)
    }
    
    this.ctx.restore()
  }
  
  drawBezier(bezierPath: BezierPath) {
    this.ctx.save()
    this.resetShapeStyles(bezierPath)
    
    if (bezierPath._transform !== undefined && bezierPath._transform !== null) {
      const { a, b, c, d, tx, ty } = bezierPath._transform
      this.ctx.transform(a, b, c, d, tx, ty)
    }
    
    const currentPoint: IPoint = { x: 0, y: 0, x1: 0, y1: 0, x2: 0, y2: 0 }
    this.ctx.beginPath()
    const d = bezierPath._d.replace(/([a-zA-Z])/g, "|||$1 ").replace(/,/g, " ")
    d.split('|||').forEach((segment: string) => {
      if (!segment.length) return
      
      const firstLetter = segment.substr(0, 1)
      if (validMethods.indexOf(firstLetter) >= 0) {
        const args = segment.substr(1).trim().split(' ')
        this.drawBezierElement(currentPoint, firstLetter, args)
      }
    })
    
    if (bezierPath._styles && bezierPath._styles.fill) this.ctx.fill()
    
    if (bezierPath._styles && bezierPath._styles.stroke) this.ctx.stroke()
    
    this.ctx.restore()
  }
  
  drawEllipse(ellipsePath: EllipsePath) {
    this.ctx.save()
    this.resetShapeStyles(ellipsePath)
    if (ellipsePath._transform !== undefined && ellipsePath._transform !== null) {
      const { a, b, c, d, tx, ty } = ellipsePath._transform
      this.ctx.transform(a, b, c, d, tx, ty)
    }
    
    const x = ellipsePath._x - ellipsePath._radiusX
    const y = ellipsePath._y - ellipsePath._radiusY
    const w = ellipsePath._radiusX * 2
    const h = ellipsePath._radiusY * 2
    const kappa = 0.5522848
    const ox = (w / 2) * kappa
    const oy = (h / 2) * kappa
    const xe = x + w
    const ye = y + h
    const xm = x + w / 2
    const ym = y + h / 2
    
    this.ctx.beginPath()
    this.ctx.moveTo(x, ym)
    this.ctx.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y)
    this.ctx.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym)
    this.ctx.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye)
    this.ctx.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym)
    
    if (ellipsePath._styles && ellipsePath._styles.fill) this.ctx.fill()
    
    if (ellipsePath._styles && ellipsePath._styles.stroke) this.ctx.stroke()
    
    this.ctx.restore()
  }
  
  drawRect(rectPath: RectPath) {
    this.ctx.save()
    this.resetShapeStyles(rectPath)
    if (rectPath._transform !== undefined && rectPath._transform !== null) {
      const { a, b, c, d, tx, ty } = rectPath._transform
      this.ctx.transform(a, b, c, d, tx, ty)
    }
    
    const x = rectPath._x
    const y = rectPath._y
    const w = rectPath._width
    const h = rectPath._height
    let radius = rectPath._cornerRadius
    
    if (w < radius * 2) radius = w / 2
    if (h < radius * 2) radius = h / 2
    
    this.ctx.beginPath()
    this.ctx.moveTo(x + radius, y)
    this.ctx.arcTo(x + w, y, x + w, y + h, radius)
    this.ctx.arcTo(x + w, y + h, x, y + h, radius)
    this.ctx.arcTo(x, y + h, x, y, radius)
    this.ctx.arcTo(x, y, x + w, y, radius)
    this.ctx.closePath()
    
    if (rectPath._styles && rectPath._styles.fill) this.ctx.fill()
    
    if (rectPath._styles && rectPath._styles.stroke) this.ctx.stroke()
    
    this.ctx.restore()
  }
  
  resetShapeStyles(bezierPath: BezierPath) {
    const styles = bezierPath._styles
    if (!styles) return
    
    if (styles && styles.stroke) {
      const r = (styles.stroke[0] * 255).toFixed(0)
      const g = (styles.stroke[1] * 255).toFixed(0)
      const b = (styles.stroke[2] * 255).toFixed(0)
      const a = styles.stroke[3]
      this.ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, ${a})`
    } else {
      this.ctx.strokeStyle = 'transparent'
    }
    
    if (styles) {
      this.ctx.lineWidth = styles.strokeWidth || undefined
      this.ctx.lineCap = styles.lineCap || undefined
      this.ctx.lineJoin = styles.lineJoin || undefined
      this.ctx.miterLimit = styles.miterLimit || undefined
    }
    
    if (styles && styles.fill) {
      const r = (styles.fill[0] * 255).toFixed(0)
      const g = (styles.fill[1] * 255).toFixed(0)
      const b = (styles.fill[2] * 255).toFixed(0)
      const a = styles.fill[3]
      this.ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${a})`
    } else {
      this.ctx.fillStyle = 'transparent'
    }
    
    // if (styles && styles.lineDash) this.ctx.lineDashOffset = styles.lineDash[2]
    if (styles && styles.lineDash) {
      this.ctx.setLineDash([styles.lineDash[0], styles.lineDash[1]], styles.lineDash[2])
    }
  }
  
  drawBezierElement(currentPoint: IPoint, method: string, args: string[]) {
    switch (method) {
      case 'M':
        currentPoint.x = Number(args[0])
        currentPoint.y = Number(args[1])
        this.ctx.moveTo(currentPoint.x, currentPoint.y)
        break
      case 'm':
        currentPoint.x += Number(args[0])
        currentPoint.y += Number(args[1])
        this.ctx.moveTo(currentPoint.x, currentPoint.y)
        break
      case 'L':
        currentPoint.x = Number(args[0])
        currentPoint.y = Number(args[1])
        this.ctx.lineTo(currentPoint.x, currentPoint.y)
        break
      case 'l':
        currentPoint.x += Number(args[0])
        currentPoint.y += Number(args[1])
        this.ctx.lineTo(currentPoint.x, currentPoint.y)
        break
      case 'H':
        currentPoint.x = Number(args[0])
        this.ctx.lineTo(currentPoint.x, currentPoint.y)
        break
      case 'h':
        currentPoint.x += Number(args[0])
        this.ctx.lineTo(currentPoint.x, currentPoint.y)
        break
      case 'V':
        currentPoint.y = Number(args[0])
        this.ctx.lineTo(currentPoint.x, currentPoint.y)
        break
      case 'v':
        currentPoint.y += Number(args[0])
        this.ctx.lineTo(currentPoint.x, currentPoint.y)
        break
      case 'C':
        currentPoint.x1 = Number(args[0])
        currentPoint.y1 = Number(args[1])
        currentPoint.x2 = Number(args[2])
        currentPoint.y2 = Number(args[3])
        currentPoint.x = Number(args[4])
        currentPoint.y = Number(args[5])
        this.ctx.bezierCurveTo(currentPoint.x1, currentPoint.y1, currentPoint.x2, currentPoint.y2, currentPoint.x, currentPoint.y)
        break
      case 'c':
        currentPoint.x1 = currentPoint.x + Number(args[0])
        currentPoint.y1 = currentPoint.y + Number(args[1])
        currentPoint.x2 = currentPoint.x + Number(args[2])
        currentPoint.y2 = currentPoint.y + Number(args[3])
        currentPoint.x += Number(args[4])
        currentPoint.y += Number(args[5])
        this.ctx.bezierCurveTo(currentPoint.x1, currentPoint.y1, currentPoint.x2, currentPoint.y2, currentPoint.x, currentPoint.y)
        break
      case 'S':
        if (currentPoint.x1 && currentPoint.y1 && currentPoint.x2 && currentPoint.y2) {
          currentPoint.x1 = currentPoint.x - currentPoint.x2 + currentPoint.x
          currentPoint.y1 = currentPoint.y - currentPoint.y2 + currentPoint.y
          currentPoint.x2 = Number(args[0])
          currentPoint.y2 = Number(args[1])
          currentPoint.x = Number(args[2])
          currentPoint.y = Number(args[3])
          this.ctx.bezierCurveTo(currentPoint.x1, currentPoint.y1, currentPoint.x2, currentPoint.y2, currentPoint.x, currentPoint.y)
        } else {
          currentPoint.x1 = Number(args[0])
          currentPoint.y1 = Number(args[1])
          currentPoint.x = Number(args[2])
          currentPoint.y = Number(args[3])
          this.ctx.quadraticCurveTo(currentPoint.x1, currentPoint.y1, currentPoint.x, currentPoint.y)
        }
        break
      case 's':
        if (currentPoint.x1 && currentPoint.y1 && currentPoint.x2 && currentPoint.y2) {
          currentPoint.x1 = currentPoint.x - currentPoint.x2 + currentPoint.x
          currentPoint.y1 = currentPoint.y - currentPoint.y2 + currentPoint.y
          currentPoint.x2 = currentPoint.x + Number(args[0])
          currentPoint.y2 = currentPoint.y + Number(args[1])
          currentPoint.x += Number(args[2])
          currentPoint.y += Number(args[3])
          this.ctx.bezierCurveTo(currentPoint.x1, currentPoint.y1, currentPoint.x2, currentPoint.y2, currentPoint.x, currentPoint.y)
        } else {
          currentPoint.x1 = currentPoint.x + Number(args[0])
          currentPoint.y1 = currentPoint.y + Number(args[1])
          currentPoint.x += Number(args[2])
          currentPoint.y += Number(args[3])
          this.ctx.quadraticCurveTo(currentPoint.x1, currentPoint.y1, currentPoint.x, currentPoint.y)
        }
        break
      case 'Q':
        currentPoint.x1 = Number(args[0])
        currentPoint.y1 = Number(args[1])
        currentPoint.x = Number(args[2])
        currentPoint.y = Number(args[3])
        this.ctx.quadraticCurveTo(currentPoint.x1, currentPoint.y1, currentPoint.x, currentPoint.y)
        break
      case 'q':
        currentPoint.x1 = currentPoint.x + Number(args[0])
        currentPoint.y1 = currentPoint.y + Number(args[1])
        currentPoint.x += Number(args[2])
        currentPoint.y += Number(args[3])
        this.ctx.quadraticCurveTo(currentPoint.x1, currentPoint.y1, currentPoint.x, currentPoint.y)
        break
      case "R":
        break
      case "r":
        break
      case "Z":
      case "z":
        this.ctx.closePath()
        break
      default:
        break
    }
  }
  
  setGlobalTransform(transform: ITransform) {
    this.globalTransform = transform
  }
  
  setDynamicImage(dynamiImage: { [key: string]: any }) {
    this.dynamicImage = dynamiImage
  }
  
  setDynamicText(dynamicText: { [key: string]: IDynamicText }) {
    this.dynamicText = dynamicText
  }
}